﻿using System;
using System.Diagnostics;
using RayDen.Library.Components.Surface;
using RayDen.Library.Core;
using RayDen.Library.Core.Primitives;
using RayDen.Library.Entity.Scene;
using RayDen.RayEngine.Core.Interface;
using RayDen.RayEngine.Core.Types;

using Spectrum = RayDen.Library.Core.Primitives.RgbSpectrum;

namespace RayDen.RayEngine.Core.Surface
{
    public abstract class BaseBxdf : BrdfBase
    {
        public HitPointInfo HitPoint;
        public IRayEngineScene Scene;
        public MaterialInfo MaterialInfo;
        public ONB Frame;
        public SurfaceTextureInfo TexData;
        protected bool LightMaterial;

        public abstract bool IsDelta { get; }

        protected BaseBxdf() { }


        protected BaseBxdf(ref RayHit rh, ref RayData ray, ref Normal ng, ref Normal ns, ref UV texCoord,
                MaterialInfo mi, SurfaceTextureInfo texData, bool fromLight)
        {
#if VERBOSE
            if (ng.Length > 1f || ns.Length > 1f)
            {
                Console.WriteLine("Normals in bsdf arent normalized");
            }
#endif
            this.Init(ref rh, ref ray, ref ng, ref ns, ref texCoord, mi, texData, fromLight);
        }

        public void SetLight(bool l)
        {
            this.LightMaterial = l;
        }

        internal void Init(ref RayHit rh, ref RayData ray, ref Normal ng, ref Normal ns, ref UV texCoord,
            MaterialInfo mi, SurfaceTextureInfo texData, bool fromLight)
        {
            if (rh.Miss())
            {
                throw new ArgumentException("RayHit missed geometry!");
            }
            //if (HitPoint == null)
            //{
                HitPoint = new HitPointInfo
                {
                    Color = RgbSpectrum.Max(ref mi.Kd, ref mi.Ks),
                    HitPoint = ray.Point(rh.Distance),
                    TexCoord = texCoord,
                    FromDirection = -ray.Dir,
                    GeoNormal = ng,
                    ShadingNormal = ns,
                    FromLight = fromLight
                };
            //}
            //else
            //{
            //    HitPoint.HitPoint = ray.Point(rh.Distance);
            //    HitPoint.TexCoord = texCoord;
            //    HitPoint.FromDirection = -ray.Dir;
            //    HitPoint.GeoNormal = ng;
            //    HitPoint.ShadingNormal = ns;
            //    HitPoint.FromLight = fromLight;
            //};
            this.MaterialInfo = mi;
            this.TexData = texData;
            if (Frame == null)
                this.Frame = new ONB(ref HitPoint.GeoNormal);
            else
                Frame.SetFromZ(ref HitPoint.GeoNormal);
        }

        public override bool IsLightSource()
        {
            if (LightMaterial)
                return true;
            return Type.Has(BrdfType.Lightsource);
        }

        public override bool IsDiffuse()
        {
            return Type.Has(BrdfType.Diffuse);
        }

        public override bool IsRefractive()
        {
            return Type.Has(BrdfType.Refractive);
        }

        public override bool IsSpecular()
        {
            return Type.Has(BrdfType.Specular);
        }

        public void Pdf(ref Vector sampledDir, out float directPdfW, out float reversePdfW)
        {
            directPdfW = 0f;
            reversePdfW = 0f;
        }

        //luxrays::Spectrum GetEmittedRadiance(float *directPdfA = NULL, float *emissionPdfW = NULL) const ;

        public RgbSpectrumInfo Evaluate(ref Vector generatedDir, out BsdfEvent _event, out float directPdfW,
            out float reversePdfW)
        {
            _event = this.Type.TypeToEvent();
            directPdfW = 0f;
            reversePdfW = 0f;
            Vector eyeDir = HitPoint.FromLight ? generatedDir : HitPoint.FromDirection;
            Vector lightDir = HitPoint.FromLight ? HitPoint.FromDirection : generatedDir;

            float dotLightDirNG = Vector.Dot(ref lightDir, ref HitPoint.GeoNormal);
            float absDotLightDirNG = Math.Abs(dotLightDirNG);
            float dotEyeDirNG = Vector.Dot(ref eyeDir, ref HitPoint.GeoNormal);
            float absDotEyeDirNG = Math.Abs(dotEyeDirNG);

            if ((absDotLightDirNG < MathLab.COS_EPSILON_STATIC) ||
                (absDotEyeDirNG < MathLab.COS_EPSILON_STATIC))
                return RgbSpectrumInfo.Black;

            float sideTest = dotEyeDirNG * dotLightDirNG;
            if (((sideTest > 0f) && !(this.Type.Has(BrdfType.Reflection))) ||(((sideTest < 0f) && !(this.Type.Has(BrdfType.Refractive))))) return RgbSpectrumInfo.Black;
 

            Vector localLightDir = Frame.ToLocal(ref lightDir);
            Vector localEyeDir = Frame.ToLocal(ref eyeDir);
            RgbSpectrumInfo result = this.Evaluate(ref HitPoint, ref localLightDir, ref localEyeDir, out _event, out directPdfW, out reversePdfW);

          

#if VERBOSE
             RayDen.Library.Core.Components.Assert.IsNotNaN(result.c1);
            RayDen.Library.Core.Components.Assert.IsNotNaN(result.c2);
            RayDen.Library.Core.Components.Assert.IsNotNaN(result.c3);
#endif
            // Adjoint BSDF
            if (HitPoint.FromLight)
            {
                float absDotLightDirNS = Vector.AbsDot(ref lightDir, ref HitPoint.ShadingNormal);
                float absDotEyeDirNS = Vector.AbsDot(ref eyeDir, ref HitPoint.ShadingNormal);
                return result * ((absDotLightDirNS * absDotEyeDirNG) / (absDotEyeDirNS * absDotLightDirNG));
            }
            else
                return result;
        }


        public RgbSpectrum Sample(out Vector dir, float u0, float u1, float u2, out float pdfW,
            out float absCosDir, out BsdfEvent _event)
        {
            Vector localFixedDir = Frame.ToLocal(ref HitPoint.FromDirection);
            Vector localSampledDir;
            dir = new Vector();
            var result = this.Sample(ref HitPoint,
                ref localFixedDir, out localSampledDir, u0, u1,
                out pdfW, out absCosDir, out _event);
            if (result.IsBlack())
                return result;

            dir = Frame.ToWorld(ref localSampledDir);
#if VERBOSE
            RayDen.Library.Core.Components.Assert.IsNotNaN(result.c1);
            RayDen.Library.Core.Components.Assert.IsNotNaN(result.c2);
            RayDen.Library.Core.Components.Assert.IsNotNaN(result.c3);
#endif
            // Adjoint BSDF
            if (HitPoint.FromLight)
            {
                float absDotFixedDirNS = Math.Abs(localFixedDir.z);
                float absDotSampledDirNS = Math.Abs(localSampledDir.z);
                float absDotFixedDirNG = Vector.AbsDot(ref HitPoint.FromDirection, ref HitPoint.GeoNormal);
                float absDotSampledDirNG = Vector.AbsDot(ref dir, ref HitPoint.GeoNormal);

                return result * ((absDotFixedDirNS * absDotSampledDirNG) / (absDotSampledDirNS * absDotFixedDirNG));
            }
            else
                return result;

        }

        protected abstract RgbSpectrum Sample(ref HitPointInfo HitPoint, ref Vector localFixedDir,
            out Vector localSampledDir, float u0, float u1, out float pdfW, out float absCosDir, out BsdfEvent _event);


        protected abstract RgbSpectrumInfo Evaluate(ref HitPointInfo HitPoint, ref Vector localLightDir,
            ref Vector localEyeDir, out BsdfEvent _event, out float directPdfW, out float reversePdfW);


        protected float Sgn(float f)
        {
            
            
            if (f < 0)
                return -1;
            return 1;
        }


        protected static Spectrum FrFull(float cosi, Spectrum cost, Spectrum eta, Spectrum k)
        {
            Spectrum tmp = (eta * eta + k * k) * (cosi * cosi) + (cost * cost);
            Spectrum Rparl2 = (tmp - (2f * cosi * cost) * eta) /
               (tmp + (2f * cosi * cost) * eta);
            Spectrum tmp_f = (eta * eta + k * k) * (cost * cost) + (cosi * cosi);
            Spectrum Rperp2 = (tmp_f - (2f * cosi * cost) * eta) /
               (tmp_f + (2f * cosi * cost) * eta);
            return (Rparl2 + Rperp2) * .5f;
        }
        protected static Spectrum FrDiel2(float cosi, Spectrum cost,
               Spectrum eta)
        {
            Spectrum Rparl = (eta * cosi);
            Rparl = (cost - Rparl) / (cost + Rparl);
            Spectrum Rperp = (eta * cost);
            Rperp = (new Spectrum(cosi) - Rperp) / (new Spectrum(cosi) + Rperp);

            return (Rparl * Rparl + Rperp * Rperp) * .5f;
        }

        protected Spectrum FresnelGeneral_Evaluate(Spectrum eta, Spectrum k, float cosi)
        {
            Spectrum sint2 = new Spectrum(Math.Max(0f, 1f - cosi * cosi));
            if (cosi > 0f)
                sint2 /= eta * eta;
            else
                sint2 *= eta * eta;
            sint2 = sint2.Clamp(0f, 1f);

            Spectrum cost2 = (Spectrum.UnitSpectrum() - sint2);
            if (cosi > 0f)
            {
                Spectrum a = 2f * k * k * sint2;
                return FrFull(cosi, ((cost2 + ((cost2 * cost2 + a * a).Sqrt())) / 2f).Sqrt(), eta, k);
            }
            else
            {
                Spectrum a = (2f * k * k * sint2);
                Spectrum d2 = eta * eta + k * k;
                return FrFull(-cosi, ((cost2 + (cost2 * cost2 + a * a).Sqrt()) / 2f).Sqrt(), eta / d2, (-1 * k) / d2);
            }
        }

        protected Spectrum FresnelCauchy_Evaluate(float eta, float cosi)
        {
            // Compute indices of refraction for dielectric
            bool entering = (cosi > 0f);

            // Compute _sint_ using Snell's law
            float eta2 = eta * eta;
            float sint2 = (entering ? 1f / eta2 : eta2) * Math.Max(0f, 1f - cosi * cosi);
            // Handle total internal reflection
            if (sint2 >= 1f)
                return new Spectrum(1f);
            else
                return FrDiel2(Math.Abs(cosi), new Spectrum(MathLab.Sqrt(Math.Max(0f, 1f - sint2))), entering ? new Spectrum(eta) : (new Spectrum(1f) / eta));
        }
    }

}
